//! Simple module to store files in database.
//!
//! docs.rs supports two ways of storing files: in a postgres database and in an S3 bucket.
//! It does not support storing files on disk because of the sheer number of files:
//! doing so would quickly run into file descriptor limits when running the web server.
//!
//! It's recommended that you use the S3 bucket in production to avoid running out of disk space.
//! However, postgres is still available for testing and backwards compatibility.

use crate::error::Result;
use crate::{
    db::mimes,
    storage::{AsyncStorage, CompressionAlgorithm},
};
use mime::Mime;
use serde_json::Value;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use tracing::instrument;

/// represents a file path from our source or documentation builds.
/// Used to return metadata about the file.
#[derive(Debug)]
pub struct FileEntry {
    pub(crate) path: PathBuf,
    pub(crate) size: u64,
}

impl FileEntry {
    pub(crate) fn mime(&self) -> Mime {
        detect_mime(&self.path)
    }
}

pub(crate) fn detect_mime(file_path: impl AsRef<Path>) -> Mime {
    let mime = mime_guess::from_path(file_path.as_ref())
        .first()
        .unwrap_or(mime::TEXT_PLAIN);

    match mime.as_ref() {
        "text/plain" | "text/troff" | "text/x-markdown" | "text/x-rust" | "text/x-toml" => {
            match file_path.as_ref().extension().and_then(OsStr::to_str) {
                Some("md") => mimes::TEXT_MARKDOWN.clone(),
                Some("rs") => mimes::TEXT_RUST.clone(),
                Some("markdown") => mimes::TEXT_MARKDOWN.clone(),
                Some("css") => mime::TEXT_CSS,
                Some("toml") => mimes::TEXT_TOML.clone(),
                Some("js") => mime::TEXT_JAVASCRIPT,
                Some("json") => mime::APPLICATION_JSON,
                Some("gz") => mimes::APPLICATION_GZIP.clone(),
                Some("zst") => mimes::APPLICATION_ZSTD.clone(),
                _ => mime,
            }
        }
        "image/svg" => mime::IMAGE_SVG,

        _ => mime,
    }
}

/// Store all files in a directory and return [[mimetype, filename]] as Json
///
/// If there is an S3 Client configured, store files into an S3 bucket;
/// otherwise, stores files into the 'files' table of the local database.
///
/// The mimetype is detected using `magic`.
///
/// Note that this function is used for uploading both sources
/// and files generated by rustdoc.
pub async fn add_path_into_database<P: AsRef<Path>>(
    storage: &AsyncStorage,
    prefix: impl AsRef<Path>,
    path: P,
) -> Result<(Vec<FileEntry>, CompressionAlgorithm)> {
    storage.store_all(prefix.as_ref(), path.as_ref()).await
}

#[instrument(skip(storage))]
pub async fn add_path_into_remote_archive<P: AsRef<Path> + std::fmt::Debug>(
    storage: &AsyncStorage,
    archive_path: &str,
    path: P,
    public_access: bool,
) -> Result<(Vec<FileEntry>, CompressionAlgorithm)> {
    let (file_list, algorithm) = storage
        .store_all_in_archive(archive_path, path.as_ref())
        .await?;
    if public_access {
        storage.set_public_access(archive_path, true).await?;
    }
    Ok((file_list, algorithm))
}

pub(crate) fn file_list_to_json(files: impl IntoIterator<Item = FileEntry>) -> Value {
    Value::Array(
        files
            .into_iter()
            .map(|info| {
                Value::Array(vec![
                    Value::String(info.mime().as_ref().to_string()),
                    Value::String(info.path.into_os_string().into_string().unwrap()),
                    Value::Number(info.size.into()),
                ])
            })
            .collect(),
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use test_case::test_case;

    // some standard mime types that mime-guess handles
    #[test_case("txt", &mime::TEXT_PLAIN)]
    #[test_case("html", &mime::TEXT_HTML)]
    // overrides of other mime types and defaults for
    // types mime-guess doesn't know about
    #[test_case("md", &mimes::TEXT_MARKDOWN)]
    #[test_case("rs", &mimes::TEXT_RUST)]
    #[test_case("markdown", &mimes::TEXT_MARKDOWN)]
    #[test_case("css", &mime::TEXT_CSS)]
    #[test_case("toml", &mimes::TEXT_TOML)]
    #[test_case("js", &mime::TEXT_JAVASCRIPT)]
    #[test_case("json", &mime::APPLICATION_JSON)]
    #[test_case("zst", &mimes::APPLICATION_ZSTD)]
    #[test_case("gz", &mimes::APPLICATION_GZIP)]
    fn test_detect_mime(ext: &str, expected: &Mime) {
        assert_eq!(&detect_mime(format!("something.{ext}")), expected);
    }
}
